package com.lipaluma.context.mapping.impl;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;

import com.lipaluma.annotations.mapping.MapMultiplePathes;
import com.lipaluma.annotations.mapping.MapPath;
import com.lipaluma.annotations.merge.OneToMany;
import com.lipaluma.context.mapping.AnnotationMappingManager;
import com.lipaluma.context.mapping.XmlMappingManager;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.mapper.AutomaticFieldMapper;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.factory.FieldMapperFactory;
import com.lipaluma.field.mapper.factory.MultipleFieldMapperFactory;
import com.lipaluma.field.mapper.mapping.MappingOptions;
import com.lipaluma.field.mapper.mapping.Path;
import com.lipaluma.field.mapper.single.impl.OneToManyFieldMapper;
import com.lipaluma.util.ReflectionUtils;
import com.lipaluma.xml.mapping.XmlFieldMapping;
import com.lipaluma.xml.mapping.XmlMultiplePathesMapping;
import com.lipaluma.xml.mapping.XmlPathMapping;

public class PathMappingManager implements AnnotationMappingManager, XmlMappingManager {
	private Map<Field, XmlFieldMapping> xmlMappingByField = new HashMap<Field, XmlFieldMapping>();
	
	@Override
	public Path initMapping(Field field) {
		if(field.isAnnotationPresent(MapPath.class)) {
			MapPath annotation = field.getAnnotation(MapPath.class);
			if(!annotation.path().trim().isEmpty())
				return new Path(annotation.path().trim(), annotation.mandatoryPresence());
			else 
				return new Path(field.getName(), annotation.mandatoryPresence());
		} else 
			return new Path(field.getName());
	}
	
	@Override
	public List<Path> initMultipleMappings(Field field){
		if(!isMultipleMapping(field))
			throw new CsvParserException("Error on field '"+field.getName()+"' : The field mapper choosen needs the presence of the annotation MapMultiplePathes on the field");
		List<Path> pathes = new ArrayList<Path>();
		MapMultiplePathes annotation = field.getAnnotation(MapMultiplePathes.class);
		for(String path : annotation.pathes())
			pathes.add(new Path(path, annotation.mandatoryPresence()));
		return pathes;
	}
	
	@Override
	public MappingOptions initOptions(Field field) {
		MappingOptions.Builder builder = MappingOptions.builder();
		if(field.isAnnotationPresent(MapMultiplePathes.class)) {
			MapMultiplePathes annotation = field.getAnnotation(MapMultiplePathes.class);
			builder.isRequired(annotation.required());
			builder.isTrimed(annotation.trim());
			builder.withPattern(annotation.pattern());
			builder.withLocale(annotation.locale());
			builder.withSeparator(annotation.separator());
			builder.isAllkeeped(annotation.keepAll());
			builder.isEachValueRequired(annotation.eachValueRequired());
			return builder.build();
		} else if(field.isAnnotationPresent(MapPath.class)) {
			MapPath annotation = field.getAnnotation(MapPath.class);
			builder.isRequired(annotation.required());
			builder.isTrimed(annotation.trim());
			builder.withPattern(annotation.pattern());
			builder.withLocale(annotation.locale());
			builder.withSeparator(annotation.separator());
			builder.isAllkeeped(annotation.keepAll());
			builder.withTrueValues(annotation.trueValues());
			builder.withFalseValues(annotation.falseValues());
			return builder.build();
		}
		return null;
	}

	@Override
	public Path initMapping(Field field, XmlFieldMapping xmlMapping) {
		xmlMappingByField.put(field, xmlMapping);
		XmlPathMapping xmlPath = (XmlPathMapping) xmlMapping;
		if(StringUtils.isNotBlank(xmlPath.getPath()))
			return new Path(xmlPath.getPath(), xmlPath.isPresenceMandatory());
		else
			return new Path(field.getName(), xmlPath.isPresenceMandatory());
	}

	@Override 
	public List<Path> initMultipleMappings(Field field, XmlFieldMapping xmlMapping) {
		if(!xmlMapping.isMultiple())
			throw new CsvParserException("Error on field '"+field.getName()+"' : the field mapper needs a multiple mapping");
		xmlMappingByField.put(field, xmlMapping);
		XmlMultiplePathesMapping mapping = (XmlMultiplePathesMapping)xmlMapping;
		List<Path> pathes = new ArrayList<Path>();
		if(mapping.getPathes().length > 0) {
			for (String path : mapping.getPathes()) {
				if(StringUtils.isBlank(path))
					throw new CsvParserException("Error on field '"+field.getName()+"' : a path is empty");
				pathes.add(new Path(path.trim(), mapping.isPresenceMandatory()));
			}
			return pathes;
		} else 
			throw new CsvParserException("Error on field '"+field.getName()+"' : an attribute 'pathes' is expected on field declared 'multiple'");
	}

	@Override
	public boolean checkRequiredOnMapping(Field field) {
		if(field.isAnnotationPresent(MapMultiplePathes.class)) {
			MapMultiplePathes annotation = field.getAnnotation(MapMultiplePathes.class);
			return annotation.required();
		} else if(field.isAnnotationPresent(MapPath.class)) {
			MapPath annotation = field.getAnnotation(MapPath.class);
			return annotation.required();
		}
		return false;
	}
		
	@Override
	public boolean checkTrimedOnMapping(Field field) {
		if(field.isAnnotationPresent(MapMultiplePathes.class)) {
			MapMultiplePathes annotation = field.getAnnotation(MapMultiplePathes.class);
			return annotation.trim();
		} else if(field.isAnnotationPresent(MapPath.class)) {
			MapPath annotation = field.getAnnotation(MapPath.class);
			return annotation.trim();
		}
		return false;
	}

	@Override
	public FieldMapper createFieldMapper(Field field) {
		Class<? extends FieldMapper> fieldMapperClass = findFieldMapperClass(field);
		FieldMapper fieldMapper = FieldMapperFactory.instantiateFieldMapper(fieldMapperClass);			
		if(isOneToMany(field))
			return new OneToManyFieldMapper(fieldMapper);
		return fieldMapper;
	}

	@Override
	public FieldMapper createFieldMapper(Field field, XmlFieldMapping mapping) {
		Class<? extends FieldMapper> fieldMapperClass = findFieldMapperClass(field, mapping);
		FieldMapper fieldMapper = FieldMapperFactory.instantiateFieldMapper(fieldMapperClass);
		if(mapping.isOneToMany())
			return new OneToManyFieldMapper(fieldMapper);
		return fieldMapper;
	}

	@Override
	public Class<?> getTargetClass(Field field) {
		Class<?> targetClass;
		if(isOneToMany(field))
			targetClass = ReflectionUtils.getParameterizedClass(field);
		else
			targetClass = field.getType();
		return targetClass;
	}	

	@Override
	public Class<?> getTargetClass(Field field, XmlFieldMapping mapping) {
		Class<?> targetClass;
		if(mapping.isOneToMany())
			targetClass = ReflectionUtils.getParameterizedClass(field);
		else
			targetClass = field.getType();
		return targetClass;
	}
	
	public boolean isMultipleMapping(Field field) {
		return field.isAnnotationPresent(MapMultiplePathes.class);
	}	


	public Class<? extends FieldMapper> findFieldMapperClass(Field field) {
		Class<?> targetClass = getTargetClass(field);
		if(isOneToMany(field)) {
			OneToMany oneToMany = field.getAnnotation(OneToMany.class);
			if(AutomaticFieldMapper.class.equals(oneToMany.with()))
				return FieldMapperFactory.retrieveFieldMapperClassForType(targetClass);
			else 
				return oneToMany.with();
		}if(field.isAnnotationPresent(MapPath.class)) {
			if(AutomaticFieldMapper.class.equals(field.getAnnotation(MapPath.class).with()))
				return FieldMapperFactory.retrieveFieldMapperClassForType(targetClass);
			else 
				return field.getAnnotation(MapPath.class).with();
		}
		else if(field.isAnnotationPresent(MapMultiplePathes.class))
			if(AutomaticFieldMapper.class.equals(field.getAnnotation(MapMultiplePathes.class).with()))
				return MultipleFieldMapperFactory.retrieveMultipleFieldMapperClassForType(targetClass);
			else
				return field.getAnnotation(MapMultiplePathes.class).with();
		else
			return FieldMapperFactory.retrieveFieldMapperClassForType(targetClass);
	}	

	public Class<? extends FieldMapper> findFieldMapperClass(Field field, XmlFieldMapping mapping) {
		Class<?> targetClass = getTargetClass(field, mapping);		
		if(mapping.isMultiple()) {
			XmlMultiplePathesMapping multipleMapping = (XmlMultiplePathesMapping)mapping;
			if(AutomaticFieldMapper.class.equals(multipleMapping.with()))
				return MultipleFieldMapperFactory.retrieveMultipleFieldMapperClassForType(targetClass);
			else
				return multipleMapping.with();
		}
		else {
			if(AutomaticFieldMapper.class.equals(mapping.with()))
				return FieldMapperFactory.retrieveFieldMapperClassForType(targetClass);
			else 
				return mapping.with();
		}
	}

	public boolean isOneToMany(Field field) {
		return field.isAnnotationPresent(OneToMany.class);
	}
}
